//
// Battery.swift
// SystemKit
//
// The MIT License
//
// Copyright (C) 2014-2017  beltex <https://github.com/beltex>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import IOKit
import Foundation

/**
 Battery statistics for OS X, read-only.
 
 http://www.apple.com/batteries/
 
 TODO: None of this will work for iOS as I/O Kit is a private framework there
 */
public struct Battery {
  
  //--------------------------------------------------------------------------
  // MARK: PUBLIC ENUMS
  //--------------------------------------------------------------------------
  
  
  /// Temperature units
  public enum TemperatureUnit {
    case celsius
    case fahrenheit
    case kelvin
  }
  
  
  //--------------------------------------------------------------------------
  // MARK: PRIVATE ENUMS
  //--------------------------------------------------------------------------
  
  
  /// Battery property keys. Sourced via 'ioreg -brc AppleSmartBattery'
  fileprivate enum Key: String {
    case ACPowered        = "ExternalConnected"
    case Amperage         = "Amperage"
    /// Current charge
    case CurrentCapacity  = "CurrentCapacity"
    case CycleCount       = "CycleCount"
    /// Originally DesignCapacity == MaxCapacity
    case DesignCapacity   = "DesignCapacity"
    case DesignCycleCount = "DesignCycleCount9C"
    case FullyCharged     = "FullyCharged"
    case IsCharging       = "IsCharging"
    /// Current max charge (this degrades over time)
    case MaxCapacity      = "MaxCapacity"
    case Temperature      = "Temperature"
    /// Time remaining to charge/discharge
    case TimeRemaining    = "TimeRemaining"
  }
  
  
  //--------------------------------------------------------------------------
  // MARK: PRIVATE PROPERTIES
  //--------------------------------------------------------------------------
  
  
  /// Name of the battery IOService as seen in the IORegistry
  fileprivate static let IOSERVICE_BATTERY = "AppleSmartBattery"
  
  
  fileprivate var service: io_service_t = 0
  
  
  //--------------------------------------------------------------------------
  // MARK: PUBLIC INITIALIZERS
  //--------------------------------------------------------------------------
  
  
  public init() { }
  
  
  //--------------------------------------------------------------------------
  // MARK: PUBLIC METHODS
  //--------------------------------------------------------------------------
  
  
  /**
   Open a connection to the battery.
   
   :returns: kIOReturnSuccess on success.
   */
  public mutating func open() -> kern_return_t {
    if (service != 0) {
      #if DEBUG
        print("WARNING - \(#file):\(#function) - " +
          "\(Battery.IOSERVICE_BATTERY) connection already open")
      #endif
      return kIOReturnStillOpen
    }
    
    
    // TODO: Could there be more than one service? serveral batteries?
    service = IOServiceGetMatchingService(kIOMasterPortDefault,
                                          IOServiceNameMatching(Battery.IOSERVICE_BATTERY))
    
    if (service == 0) {
      #if DEBUG
        print("ERROR - \(#file):\(#function) - " +
          "\(Battery.IOSERVICE_BATTERY) service not found")
      #endif
      return kIOReturnNotFound
    }
    
    return kIOReturnSuccess
  }
  
  
  /**
   Close the connection to the battery.
   
   :returns: kIOReturnSuccess on success.
   */
  public mutating func close() -> kern_return_t {
    let result = IOObjectRelease(service)
    service    = 0     // Reset this incase open() is called again
    
    #if DEBUG
      if (result != kIOReturnSuccess) {
        print("ERROR - \(#file):\(#function) - Failed to close")
      }
    #endif
    
    return result
  }
  
  
  //--------------------------------------------------------------------------
  // MARK: PUBLIC METHODS - BATTERY
  //--------------------------------------------------------------------------
  
  
  /**
   Get the current capacity of the battery in mAh. This is essientally the
   current charge of the battery.
   
   https://en.wikipedia.org/wiki/Ampere-hour
   */
  public func currentCapacity() -> Int {
    if let prop = IORegistryEntryCreateCFProperty(service,
                                                  Key.CurrentCapacity.rawValue as CFString,
                                                  kCFAllocatorDefault,0) {
      return prop.takeRetainedValue() as! Int
    }
    return 0
  }
  
  
  /**
   Get the current max capacity of the battery in mAh. This degrades over time
   from the original design capacity.
   
   https://en.wikipedia.org/wiki/Ampere-hour
   */
  public func maxCapactiy() -> Int {
    if let prop = IORegistryEntryCreateCFProperty(service,
                                                  Key.MaxCapacity.rawValue as CFString,
                                                  kCFAllocatorDefault, 0) {
      return prop.takeRetainedValue() as! Int
    }
    return 0
  }
  
  
  /**
   Get the designed capacity of the battery in mAh. This is a static value.
   The max capacity will be equal to this when new, and as it degrades over
   time, be less than this.
   
   https://en.wikipedia.org/wiki/Ampere-hour
   */
  public func designCapacity() -> Int {
    if let prop = IORegistryEntryCreateCFProperty(service,
                                                  Key.DesignCapacity.rawValue as CFString,
                                                  kCFAllocatorDefault, 0) {
      return prop.takeRetainedValue() as! Int
    }
    return 0
  }
  
  
  /**
   Get the current cycle count of the battery.
   
   https://en.wikipedia.org/wiki/Charge_cycle
   */
  public func cycleCount() -> Int {
    if let prop = IORegistryEntryCreateCFProperty(service,
                                                  Key.CycleCount.rawValue as CFString,
                                                  kCFAllocatorDefault, 0) {
      return prop.takeRetainedValue() as! Int
    }
    return 0
  }
  
  
  /**
   Get the designed cycle count of the battery.
   
   https://en.wikipedia.org/wiki/Charge_cycle
   */
  public func designCycleCount() -> Int {
    if let prop = IORegistryEntryCreateCFProperty(service,
                                                  Key.DesignCycleCount.rawValue as CFString,
                                                  kCFAllocatorDefault, 0) {
      return prop.takeRetainedValue() as! Int
    }
    
    return 0
  }
  
  
  /**
   Is the machine powered by AC? Plugged into the charger.
   
   :returns: True if it is, false otherwise.
   */
  public func isACPowered() -> Bool {
    if let prop = IORegistryEntryCreateCFProperty(service,
                                                  Key.ACPowered.rawValue as CFString,
                                                  kCFAllocatorDefault, 0) {
      return prop.takeRetainedValue() as! Bool
    }
    return false
  }
  
  
  /**
   Is the battery charging?
   
   :returns: True if it is, false otherwise.
   */
  public func isCharging() -> Bool {
    if let prop = IORegistryEntryCreateCFProperty(service,
                                                  Key.IsCharging.rawValue as CFString,
                                                  kCFAllocatorDefault, 0) {
      return prop.takeRetainedValue() as! Bool
    }
    return false
  }
  
  
  /**
   Is the battery fully charged?
   
   :returns: True if it is, false otherwise.
   */
  public func isCharged() -> Bool {
    if let prop = IORegistryEntryCreateCFProperty(service,
                                                  Key.FullyCharged.rawValue as CFString,
                                                  kCFAllocatorDefault, 0) {
      return prop.takeRetainedValue() as! Bool
    }
    return false
  }
  
  
  /**
   What is the current charge of the machine? As seen in the battery status
   menu bar. This is the currentCapacity / maxCapacity.
   
   :returns: The current charge as a % out of 100.
   */
  public func charge() -> Double {
    return floor(Double(currentCapacity()) / Double(maxCapactiy()) * 100.0)
  }
  
  
  /**
   The time remaining to full charge (if plugged into AC), or the time
   remaining to full discharge (running on battery). See also
   timeRemainingFormatted().
   
   :returns: Time remaining in minutes.
   */
  public func timeRemaining() -> Int {
    if let prop = IORegistryEntryCreateCFProperty(service,
                                                  Key.TimeRemaining.rawValue as CFString,
                                                  kCFAllocatorDefault, 0) {
      return prop.takeRetainedValue() as! Int
    }
    return 0
  }
  
  
  /**
   Same as timeRemaining(), but returns as a human readable time format, as
   seen in the battery status menu bar.
   
   :returns: Time remaining string in the format <HOURS>:<MINUTES>
   */
  public func timeRemainingFormatted() -> String {
    let time = timeRemaining()
    return NSString(format: "%d:%02d", time / 60, time % 60) as String
  }
  
  
  /**
   Get the current temperature of the battery.
   
   "MacBook works best at 50° to 95° F (10° to 35° C). Storage temperature:
   -4° to 113° F (-20° to 45° C)." - via Apple
   
   http://www.apple.com/batteries/maximizing-performance/
   
   :returns: Battery temperature, by default in Celsius.
   */
  
  public func temperature(_ unit: TemperatureUnit = .celsius) -> Double {
    var temperature: Double = 0
    if let prop = IORegistryEntryCreateCFProperty(service,
                                                  Key.Temperature.rawValue as CFString,
                                                  kCFAllocatorDefault, 0) {
      
      temperature = prop.takeRetainedValue() as! Double / 100.0
    }
    
    switch unit {
    case .celsius:
      // Do nothing - in Celsius by default
      // Must have complete switch though with executed command
      break
    case .fahrenheit:
      temperature = Battery.toFahrenheit(temperature)
    case .kelvin:
      temperature = Battery.toKelvin(temperature)
    }
    
    return ceil(temperature)
  }
  
  //--------------------------------------------------------------------------
  // MARK: PRIVATE HELPERS
  //--------------------------------------------------------------------------
  
  
  /**
   Celsius to Fahrenheit
   
   :param: temperature Temperature in Celsius
   :returns: Temperature in Fahrenheit
   */
  fileprivate static func toFahrenheit(_ temperature: Double) -> Double {
    // https://en.wikipedia.org/wiki/Fahrenheit#Definition_and_conversions
    return (temperature * 1.8) + 32
  }
  
  
  /**
   Celsius to Kelvin
   
   :param: temperature Temperature in Celsius
   :returns: Temperature in Kelvin
   */
  fileprivate static func toKelvin(_ temperature: Double) -> Double {
    // https://en.wikipedia.org/wiki/Kelvin
    return temperature + 273.15
  }
}
